package bowshot

import (
	"bufio"
	"fmt"
	"os"
	"path/filepath"
	"sync"
	"time"
)

type slotWriter struct {
	maxsize int64
	path    string
	file    *os.File
	mu      sync.Mutex
	written int64
	buf     []byte
	w       *bufio.Writer
	pid     int
	expires int
}

func (a *slotWriter) close() {
	a.mu.Lock()
	if a.file != nil {
		a.w.Flush()
		a.file.Close()
	}
	a.file = nil
	a.mu.Unlock()
}

func (a *slotWriter) open() {
	var err error
	a.w.Reset(os.Stderr)
	dir := filepath.Dir(a.path)
	if _, err := os.Stat(dir); err != nil && os.IsNotExist(err) {
		_ = os.MkdirAll(dir, 0776)
	}
	if a.file, err = os.OpenFile(a.path, os.O_CREATE|os.O_APPEND|os.O_RDWR, 0644); err != nil {
		a.file = nil
		return
	}
	if fi, err := a.file.Stat(); err == nil {
		a.written = fi.Size() // add file size record
	}
	a.w.Reset(a.file)
}

// Thanks C++ STL
func findFirstOf(s string, sep string) int {
	for i, c := range s {
		for _, c2 := range sep {
			if c2 == c {
				return i
			}
		}
	}
	return -1
}

func cleanName(s string) string {
	i := findFirstOf(s, ".-+_@() \t?")
	if i == -1 {
		return s
	}
	return s[0:i]
}

func (a *slotWriter) rotate() {
	if a.file != nil {
		a.file.Close()
		a.file = nil
	}
	dir := filepath.Dir(a.path)
	base := filepath.Base(a.path)
	ext := filepath.Ext(base)
	name := base[0 : len(base)-len(ext)]
	logdir := cleanName(name) + "-log"
	xdir := filepath.Join(dir, logdir)
	if _, err := os.Stat(xdir); err != nil {
		if err = os.Mkdir(xdir, 0777); err != nil {
			fmt.Fprintf(os.Stderr, "Mkdir: %s\n", err)
		}
	}
	now := time.Now()
	nf := fmt.Sprintf(
		"%s%c%s.%04d%02d%02d-%02d%02d-%02d%s",
		xdir,
		os.PathSeparator,
		name,
		now.Year(),
		now.Month(),
		now.Day(),
		now.Hour(),
		now.Minute(),
		now.Second(),
		ext)
	if err := os.Rename(a.path, nf); err != nil {
		fmt.Fprintf(os.Stderr, "Rename log file: %s\n", err)
	}
	if a.expires != 0 {
		go func(logdir string, expires int) {
			_ = RemoveExpired(logdir, expires)
		}(logdir, a.expires)
	}
	a.written = 0
	a.open()

}

// Cheap integer to fixed-width decimal ASCII. Give a negative width to avoid zero-padding.
func itoa(buf *[]byte, i int, wid int) {
	// Assemble decimal in reverse order.
	var b [20]byte
	bp := len(b) - 1
	for i >= 10 || wid > 1 {
		wid--
		q := i / 10
		b[bp] = byte('0' + i - q*10)
		bp--
		i = q
	}
	// i < 10
	b[bp] = byte('0' + i)
	*buf = append(*buf, b[bp:]...)
}

// formatHeader writes log header to buf in following order:
func (a *slotWriter) formatHeader(buf *[]byte, t time.Time, prefix string, pid int) {
	if len(prefix) != 0 {
		*buf = append(*buf, prefix...)
	}
	*buf = append(*buf, '[')
	itoa(buf, pid, -1)
	*buf = append(*buf, "] "...)
	year, month, day := t.Date()
	itoa(buf, year, 4)
	*buf = append(*buf, '-')
	itoa(buf, int(month), 2)
	*buf = append(*buf, '-')
	itoa(buf, day, 2)
	*buf = append(*buf, ' ')
	hour, min, sec := t.Clock()
	itoa(buf, hour, 2)
	*buf = append(*buf, ':')
	itoa(buf, min, 2)
	*buf = append(*buf, ':')
	itoa(buf, sec, 2)

	*buf = append(*buf, ' ')
}

func (a *slotWriter) formatHeaderAccess(buf *[]byte, t time.Time) {
	*buf = append(*buf, '[')
	year, month, day := t.Date()
	itoa(buf, year, 4)
	*buf = append(*buf, '-')
	itoa(buf, int(month), 2)
	*buf = append(*buf, '-')
	itoa(buf, day, 2)
	*buf = append(*buf, ' ')
	hour, min, sec := t.Clock()
	itoa(buf, hour, 2)
	*buf = append(*buf, ':')
	itoa(buf, min, 2)
	*buf = append(*buf, ':')
	itoa(buf, sec, 2)
	*buf = append(*buf, "] "...)
}

func (a *slotWriter) writetimev(flush bool, prefix, s string, t time.Time) error {
	a.mu.Lock()
	defer a.mu.Unlock()
	if a.file == nil {
		return nil
	}
	a.buf = a.buf[:0]
	a.formatHeader(&a.buf, t, prefix, a.pid)
	a.buf = append(a.buf, s...)
	if len(s) == 0 || s[len(s)-1] != '\n' {
		a.buf = append(a.buf, '\n')
	}
	a.written += int64(len(a.buf))
	if a.w.Available() < len(a.buf) {
		_ = a.w.Flush()
	}
	_, err := a.w.Write(a.buf)
	if a.written >= a.maxsize || flush {
		_ = a.w.Flush()
		if a.written >= a.maxsize {
			a.rotate() // rotate
		}
		return err
	}
	return err
}

func (a *slotWriter) writevaccess(s string, t time.Time) error {
	if a.file == nil {
		return nil
	}
	a.mu.Lock()
	defer a.mu.Unlock()
	a.buf = a.buf[:0]
	a.formatHeaderAccess(&a.buf, t)
	a.buf = append(a.buf, s...)
	if len(s) == 0 || s[len(s)-1] != '\n' {
		a.buf = append(a.buf, '\n')
	}
	if a.written >= a.maxsize {
		// TODO write a
		_ = a.w.Flush()
		a.rotate()
	}
	a.written += int64(len(a.buf))
	if a.w.Available() < len(a.buf) {
		_ = a.w.Flush()
	}
	_, err := a.w.Write(a.buf)
	return err
}

func newSlotWriter(f string, size int, maxsize int64) *slotWriter {
	a := &slotWriter{
		maxsize: maxsize,
		written: 0,
		path:    f,
		buf:     make([]byte, 4096),
		pid:     os.Getpid(),
		w:       bufio.NewWriterSize(os.Stderr, size),
	}
	a.open() //
	if a.written >= a.maxsize {
		a.rotate() // rotate
	}
	return a
}
